[org 0x1000]  ; 将loader代码放在内存0x1000的位置，是基于实模式的内存布局约束指定
dw 0x55aa  ; 调试魔数，用于判断错误，当boot读入0x1000位置时，得是0x55aa才认为正确，否则认为扇区有错误，防止错误蔓延

; 打印字符串
mov si, loading
call print
;jmp run_loader_err  ; 测试运行加载器错误打印，测试后注释去掉

; ####################检测函数标签##############
detect_memory:
    xor ebx, ebx  ; 使用异或的方式将ebx置为0（也即将要用的寄存器清空），异或是一种比较高效的方式
    ; 然后将ES段寄存器置0，ES:DI 是结构体的缓冲区
    mov ax, 0
    mov es, ax  ; 实际中，无法直接给段寄存器执行mov es,0 , 因为语法上有有歧义，不能确定是将8位还是16位的0赋值给es，而ax是确定的16位寄存器，执行本行给16位的es赋值0
    mov edi, ards_buffer
    mov edx, 0x534d4150  ; 固定签名
    ; 初始化结束后开始检测，定义个一个检测标签next
.next:
    mov eax, 0xe820  ; 指定0x15系统调用子功能号0xe820，为内存检测
    mov ecx, 20  ; ards结构的大小，单位是字节
    int 0x15  ; 调用0x15系统调用
    ; 调用开始后，不断检测返回值，直到检测结束
    jc run_loader_err  ; jc指令检测CF是否置位，如果置位，即CF=1，表示出错，则跳转到错误打印，然后退出
    ; 否则，CF=0，检测正确，则将缓存指针指向下一个结构体
    add di, cx  ; cx是ecx的低16位，前面已初始化为20，所以这里加20
    inc word [ards_count]  ; 将ards_count变量按字加1，[ards_count] 表示操作的是 ards_count 标签所指向的存储单元，间接寻址
    cmp ebx, 0  ; 每次检测后ebx与0比较，如果是0，则表示结束了，如果不是，则还要继续
    jnz .next  ; 上一行如果ebx-0!=0，则零标志位（ZF）=0,jnz就是不为0则跳转到.next标签开始继续执行，如果ebx-0==0，则零标志位（ZF）被置1，表示ebx是0，不跳转
    mov si, detect_mem_succ  ; 上一行没有跳转，则继续执行到这行，表示检测结束，将要打印的内容放入缓冲区，调用print打印
    call print  ; 打印后然后结束
    ; 为了方便观察内存检测到的结果，这里添加一些打印，读取出内存的信息
    ;xchg bx, bx  ; bochs的断点暂停调试
    mov cx, [ards_count]  ; ards_count是一个16位的内存地址，将该地址的16位数据拷贝给CX，也就是上面读到的ards结构体的数量
    mov si, 0  ; 用于结构体指针，SI（Source Index）寄存器是 16 位的通用寄存器。它通常与数据段寄存器（DS）一起使用，用于在内存寻址过程中指示源操作数的偏移地址。在 32 位模式下，它可以扩展为 32 位的 ESI 寄存器，在 64 位模式下则进一步扩展为 64 位的 RSI 寄存器。
.show:
    mov eax, [ards_buffer + si]  ; 相当于对ards_buffer数组取索引，类似于ards_buffer[si]，0到8字节是每个ards的基地址，
    mov ebx, [ards_buffer + si + 8]  ; 类似于ards_buffer[si+8]，根据格式定义，8到16字节是内存的长度
    mov edx, [ards_buffer + si + 16]  ; 这里用edx是因为ecx已经用于存[ards_count]，这里是最后4字节，是内存的类型，其中1表示可用
    ; print ards
    mov dx, 0
    .print_next:
        mov al, [ards_buffer + si]
        mov dh, al
        rol al, 4
        call covert_print_char
        mov al, dh
        call covert_print_char
        add si, 1
        add dx, 1
        cmp dx, 20
        jnz .print_next
    mov al, ','
    mov ah, 0x0e
    int 0x10
    ; add si, 20  ; 每次循环将si加一个ards结构体的长度20字节
    ;xchg bx, bx  ; bochs的断点暂停调试，每次循环后调试看下
    loop .show  ; loop指令，会自动根据cx/ecx的值判断循环次数，当cx值为0时结束循环，否则会对cx减1然后继续跳转到循环入口，直到cx减到0时结束循环
    mov si, print_mem_succ
    call print

    jmp prepare_protected_mode

jmp $  ; 阻塞

loading:
    db "loader: loading on...", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）
detect_mem_succ:  ; 检测内存成功的打印内容
    db "loader: detect memory success", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）
print_mem_succ:  ; 检测内存成功的打印内容
    db "", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）

print:
    mov ah, 0x0e ; 设置的前置条件，表示要调用int 0x10 的打印一个字符的功能
    ; 创建一个循环打印
    .next:
        mov al, [si] ; 间接寻址，将si指向的字符放入al
        cmp al, 0 ; 和0比较，如果是0则表示字符串最后一个字符，循环结束
        jz .return ; jump if zero，当零标志位ZF为1时，执行跳转操作， cmp的两个操作数的值相等（相减为0），ZF标志位会被设置为1，则跳转到.return。
        ;jz指令的另一种写法是je指令，即jump if equal，它的作用与jz指令完全相同
        int 0x10  ; 如果不是最后一个0则要调用0x10中断bios的打印功能继续打印
        inc si ; 将si+1
        jmp .next
    .return:
    ret

covert_print_char:
    and al, 0x0f
    add al, '0'
    cmp al, '9'
    jle .print_char
    add al, 7
    .print_char:
        mov ah, 0x0e
        int 0x10
    ret

run_loader_err:  ; 运行加载器错误
    mov si, .err_msg
    call print
    hlt  ; cpu停止指令
    jmp $
    .err_msg:
        db "run loader program error!", 10, 13, 0
    ret

prepare_protected_mode:
    cli ; 关闭中断，进入保护模式时需要关闭中断
    ; 1 打开a20线
    in al, 0x92 ; 打开A20线，打开方式是操作0x92端口
    or al, 0x10 ; 将第二位置1
    out 0x92, al
    ; 2 加载GDT，告诉CPU gdt在哪
    lgdt [gdt_ptr] ; 设置gdt的位置为gdt_ptr标签
    ; 3 启动保护模式，需要将cr0寄存器0位置1
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    ; 跳转到保护模式执行，用跳转来刷新缓存，因为段寄存器这些缓存未刷新，暂时还是在实模式执行，跳转后刷新缓存后才进入保护模式
    jmp dword code_selector:protect_mode_code ; 通过选择子访问内存空间

[bits 32] ; 提醒编译器已经进入32位
protect_mode_code:
    mov ax, data_selector ; 写段寄存器，将除了代码段以外的寄存器都写成数据段
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax ; 初始化好了段寄存器
    mov esp, 0x10000 ; 修改栈顶为0x10000，这个地址是随便设置的。向下增长
    ; 接下来修改1M以外的内存，改一下屏幕
    ;mov byte [0xb8000], 'P' ; 可以直接操作了，在实模式下需要通过改变段寄存器的方式才可以
    ; 原来实模式的操作：
    ;  mov ax, 0xb800
    ;  mov ds, ax
    ;  mov byte [0], 'H'
    ; 地址=段地址 * 16 + 偏移地址，实模式访问的连续内存只有64k，虽然最大1M，但不改变段基地址的情况下，只能访问到64k内的，所以代码中先改变了ds的段地址
    mov byte [0x200000], 'O' ; 再测试访问2M的内存

    ; 进入32位后，内存足够大了，可一次性将内核读入内存中，共200k
    mov edi, 0x10000 ; 数据读入目标内存0x10000位置
    mov ecx, 10 ; 在make file中，写入的是第10个扇区，所以从第10个扇区开始读
    mov bl, 200 ; 写入的扇区数量是200个，所以这里设定读入200个
    call read_disk
    jmp dword code_selector:0x10000 ; 直接跳转到这个代码段中执行
    ud2 ; 表示出错，类似于高级语言中不可能进入的分支中加的unreachable分支，这里也是表示不可能执行到此
jmp $ ; 阻塞

; 将读取硬盘的函数从boot.asm中搬过来
read_disk:
    ; 1 设置读写扇区数量
    mov dx, 0x1f2 ; 设置读取扇区数量，0x1f2是设置读写扇区的数量寄存器
    mov al, bl ; 将bl设置好的参数1写入al
    out dx, al ; 只能用这两个寄存器，设置数量为1

    inc dx ; +1后为0x1f3
    mov al, cl ; 设置起始扇区的前八位，cl是ecx的低八位，存有扇区索引0
    out dx, al

    inc dx ; +1后为0x1f4
    shr ecx, 8 ; 向右移动8位，将中八位移动到低八位的位置
    mov al, cl ; 设置起始扇区的中八位
    out dx, al

    inc dx ; +1后为0x1f5
    shr ecx, 8 ; 向右移动8位，将中八位移动到低八位的位置
    mov al, cl ; 设置起始扇区的高八位
    out dx, al

    inc dx ; +1后为0x1f6
    shr ecx, 8 ; 将最后4位移动到cl的位置，用于设置起始扇区的24-27位
    and cl, 0b1111 ; 将高四位设置为0，最后只剩下0~3位
    mov al, 0b1110_0000 ; 5-7位固定为1，第4位为0表示主盘，第6位为1表示LBA模式
    or al, cl ; 将al和cl整合
    out dx, al

    inc dx ; +1后为0x1f7
    mov al, 0x20 ; 读硬盘
    out dx, al

    xor ecx, ecx ; 清空ecx，比mov ecx, 0的方式性能更高
    mov cl, bl ; 得到读写扇区的数量， cl是cx的低8位，cx是ecx的低16位
    
    .read: ; 读扇区代码块，每次读一个扇区
        push cx ; 因为reads函数内会修改cx，所以先保存好cx
        call .waits ; 等待数据准备完毕
        call .reads ; 读取一个扇区
        pop cx ; 恢复cx
        loop .read
    ret

    .waits:
        mov dx, 0x1f7 ; 硬盘状态寄存器，检查第3位是否准备完毕标识
        .check:
            in al, dx ; 读入到al
            jmp $+2 ; 直接跳转到下一行，相当于nop指令，但这样消耗的时钟周期更多
            jmp $+2 ; 延迟手段
            jmp $+2
            and al, 0b1000_1000 ; 只留下第3和第7位，假设硬盘不会出错，所以忽略第0位，其他位设置为0
            cmp al, 0b0000_1000 ; 看是否第3位为1，第7位为0，
            jnz .check ; 不为0（不相等）则调整回去继续读入+等待，一直check，相等则不跳转，终止循环
        ret

    .reads:
        mov dx, 0x1f0
        mov cx, 256 ; 一个扇区256字
        .readw:
            in ax, dx ; 读入一个字
            jmp $+2 ; 直接跳转到下一行，相当于nop指令，但这样消耗的时钟周期更多
            jmp $+2 ; 延迟手段
            jmp $+2
            mov [edi], ax ; 将ax数据写入内存
            add edi, 2 ; 将内存指针往后移动两个字节（一个字）
            loop .readw
        ret


; 准备一些常数
memory_base equ 0 ; 内存起始地址, 基地址
memory_limit equ ((1024 * 1024 * 1024 * 4) / (1024 * 4)) - 1; 32位粒度4kb, 这里4G/4K - 1是内存界限

; ///////////////////////////////GDT全局描述符位置////////////////////////////////////////////////////////////////////////////////
; 保护模式首先定义GDT
gdt_base:
    dd 0, 0 ; NULL, dd分配四字节，CPU要求
gdt_code: ; 用于代码段
    dw memory_limit & 0xffff ; 段界限0-15位
    dw memory_base & 0xffff ; 基地址0-15位，因无法直接拆24位，需要拆成16+8的模式
    db (memory_base >> 16) & 0xff ; 基地址16-23位， 总共24位，这里因为memory_base是32位的，为了得到16-23位，所以需要右移16位
    ; 接下来是一个组合类型构成一个字节，左边高位到右边低位依次是：
    ; present 1位、DPL 2位、segment 1位、type 4位（type又进一步细分 E | C/D | R/W | A各1位）
    ; 0b表示是2进制数，高四位prensent=1存在内存上，特权DPL=0， segment=1代码/数据段，
    db 0b_1_00_1_1_0_1_0 ; 低四位E=1表示代码段，C=0非依从，R=1可读，A=0未被访问过
    ; 之后左到右对应高位到地位分别是：粒度1位、big 1位、64位扩展1位、available 1位、limit_high 4位
    ;  粒度=1表示4k颗粒度，big=1表示32位，64位扩展=0表示不是64位，available随意写，操作系统可自定义使用
    db 0b1_1_0_0_0000 | ((memory_limit >> 16) & 0xf) ; 后四位是段界限16-19位
    db (memory_base >> 24) & 0xff ; 基地址24~31位
gdt_data: ; 用于数据段
    dw memory_limit & 0xffff ; 段界限0-15位
    dw memory_base & 0xffff ; 基地址0-15位，因无法直接拆24位，需要拆成16+8的模式
    db (memory_base >> 16) & 0xff ; 基地址16-23位， 总共24位，这里因为memory_base是32位的，为了得到16-23位，所以需要右移16位
    ; 接下来是一个组合类型构成一个字节，左边高位到右边低位依次是：
    ; present 1位、DPL 2位、segment 1位、type 4位（type又进一步细分 E | C/D | R/W | A各1位）
    ; 0b表示是2进制数，高四位prensent=1存在内存上，特权DPL=0， segment=1代码/数据段，
    db 0b_1_00_1_0_0_1_0 ; 低四位E=0表示数据段，E=0向上扩展，W=1可写，A=0未被访问过 ****和代码段相比就这一行不同****
    ; 之后左到右对应高位到地位分别是：粒度1位、big 1位、64位扩展1位、available 1位、limit_high 4位
    ;  粒度=1表示4k颗粒度，big=1表示32位，64位扩展=0表示不是64位，available随意写，操作系统可自定义使用
    db 0b1_1_0_0_0000 | ((memory_limit >> 16) & 0xf) ; 后四位是段界限16-19位
    db (memory_base >> 24) & 0xff ; 基地址24~31位
gdt_end:
; ///////////////////////////描述符指针////////////////////////////////////////////////////////////////////////////////////
gdt_ptr: ; 还需要定义描述符指针
    dw (gdt_end - gdt_base) - 1 ; 首先是两个字节的limit，对应字节数
    dd gdt_base ; gdt的base，也就是gdt开始的位置
; ///////////////////////////段选择子////////////////////////////////////////////////////////////////////////////////////
; 再之后还需要段选择子
; 因为特权级RPL(2位)是0、是全局的所以TI=0（1位），所以只要index向左移3位即可，左移的3位自动补0。
code_selector equ (1 << 3) ; 用于选择代码段，代码段gdt的排在第1个（第0个是NULL），所以index=1，左移3位后RPL和TI自动补0
data_selector equ (2 << 3) ; 用于选择数据段，数据段gdt的排在第2个，所以index=2，左移3位后RPL和TI自动补0
; ///////////////////////////新增代码准备完毕//////////////////////////////////////////////////////////////////////////////

; 在最后增加两个变量数据
ards_count:  ; 用于存储内存的数量，一个字的size
    dw 0
ards_buffer:  ; 用于存储内存信息的buffer，这个一定要放在最后，因为运行前不清楚具体有多少个内存信息，
              ; 比如bochs有6个、vmware有11个，具体的实体机/虚拟机描述符的数量不定，为了防止覆盖buffer之后的内容，
              ; 放在最后才合理。若buffer溢出导致内容覆盖出错，容易导致不可预期的错误，很难调试。
